package jumio

import (
	"bytes"
	byoneconfig "code.bydev.io/cht/customer/kyc_admin_service/internal/config"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/helper"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/imgcompress"
	kae_dto "code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/kycadminevent/dto"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/kycprovider/dto"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/kycprovider/provider"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/teller"
	"code.bydev.io/frameworks/byone/core/logc"
	"code.bydev.io/frameworks/byone/rest/httpc"
	"context"
	"emperror.dev/errors"
	"encoding/json"
	"fmt"
	"git.bybit.com/svc/go/pkg/bzap"
	"git.bybit.com/svc/mod/pkg/bsecret/bsecretsecret"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	jsoniter "github.com/json-iterator/go"
	"go.uber.org/zap"
	"io/ioutil"
	"net/http"
	"strconv"
	"strings"
	"time"
)

const (
	Domain                     = "https://account.apac-1.jumio.ai"
	Token                      = "bcc55b42-aedf-4940-9c84-9276d1f7da8d"
	Secret                     = "S1kwmOKIrBz7qnLoCWF43QJ73uUnVAHc"
	DataCenter                 = "SG"
	CallBackWorkFlow           = "https://kycapi.public-test-1.bybit.com/kyc/jumio/iDVerification"
	CallBackDocument           = "https://kycapi.public-test-1.bybit.com/kyc/jumio/docVerification"
	WorkflowDomain             = "https://retrieval.apac-1.jumio.ai"
	DocumentVerificationDomain = "https://upload.core-sgp.jumio.com"
	RetrievalDomain            = "https://retrieval.core-sgp.jumio.com"
	CoreDomain                 = "https://core-sgp.jumio.com/api/netverify/v2/supportedDocumentTypes"
)

// LanguageBybitToJumioMap bybit语言映射为jumio所需语言
var LanguageBybitToJumioMap = map[string]string{
	"en-us": "en",
	"zh-cn": "zh_CN",
	"zh-tw": "zh_HK", // 由于jumio不支持台湾，则映射为en
	"ja-jp": "ja",
	"ru-ru": "ru",
	"es-es": "es",
	"vi-vn": "vi",
	"tr-tr": "tr",
	"pt-br": "pt",
	"fr-fr": "fr",
	"th-th": "th",
	"id-id": "id",
}

type JumioProvider struct {
	conf   byoneconfig.JumioConfig
	logger *zap.Logger
	tell   provider.Teller
	imgCps imgcompress.ImgCps
}

func NewJUMIOProvider(jumioConfig *byoneconfig.JumioConfig, logger *zap.Logger, tell provider.Teller) *JumioProvider {
	s := new(JumioProvider)
	if jumioConfig.Domain == "" {
		s.conf.Domain = Domain
	} else {
		s.conf.Domain = jumioConfig.Domain
	}

	if jumioConfig.WorkflowDomain == "" {
		s.conf.WorkflowDomain = WorkflowDomain
	} else {
		s.conf.WorkflowDomain = jumioConfig.WorkflowDomain
	}

	if jumioConfig.Token == "" {
		s.conf.Token = Token
	} else {
		s.conf.Token = jumioConfig.Token
	}

	if jumioConfig.Secret == "" {
		s.conf.Secret = Secret
	} else {
		s.conf.Secret = jumioConfig.Secret
	}
	if jumioConfig.DataCenter == "" {
		s.conf.DataCenter = DataCenter
	} else {
		s.conf.DataCenter = jumioConfig.DataCenter
	}

	if jumioConfig.CallBackWorkFlow == "" {
		s.conf.CallBackWorkFlow = CallBackWorkFlow
	} else {
		s.conf.CallBackWorkFlow = jumioConfig.CallBackWorkFlow
	}
	if jumioConfig.CallBackDocument == "" {
		s.conf.CallBackDocument = CallBackDocument
	} else {
		s.conf.CallBackDocument = jumioConfig.CallBackDocument
	}

	if jumioConfig.DocumentVerificationDomain == "" {
		s.conf.DocumentVerificationDomain = DocumentVerificationDomain
	} else {
		s.conf.DocumentVerificationDomain = jumioConfig.DocumentVerificationDomain
	}
	if jumioConfig.RetrievalDomain == "" {
		s.conf.RetrievalDomain = RetrievalDomain
	} else {
		s.conf.RetrievalDomain = jumioConfig.RetrievalDomain
	}

	if jumioConfig.CoreDomain == "" {
		s.conf.CoreDomain = CoreDomain
	} else {
		s.conf.CoreDomain = jumioConfig.CoreDomain
	}

	s.logger = logger
	s.tell = tell
	s.imgCps = imgcompress.New(logger, tell)
	return s
}

// Accounts 初始化工作流(workflow-3 ID+人脸验证)
func (s *JumioProvider) Accounts(ctx context.Context, userID string, verification dto.WorkflowDefinitionKeys, successURL, errorURL, language string) (*dto.AccountResponse, error) {
	var err error
	path := s.conf.Domain + "/api/v1/accounts"
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "Accounts[jumio]:"+path, err)
		}
	}()
	req := dto.AccountRequest{
		UserReference:             userID,
		CustomerInternalReference: userID,
		CallbackURL:               s.conf.CallBackWorkFlow,
		AccountWeb: &dto.AccountWeb{
			SuccessURL: successURL,
			ErrorURL:   errorURL,
			Locale:     language,
		},
		WorkflowDefinition: &dto.WorkflowDefinition{Key: verification},
	}
	if v, ok := LanguageBybitToJumioMap[language]; ok {
		req.AccountWeb.Locale = v
	} else {
		req.AccountWeb.Locale = defaultLanguageEn
	}

	body, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	fmt.Printf("req:%s\n", body)
	s.logger.Debug("Accounts[jumio]", zap.String("language:", language))
	s.logger.Debug("Accounts[jumio]", zap.String("req", string(body)))
	b, err := s.request(ctx, commonHeader, path, "POST", body)
	if err != nil {
		return nil, err
	}
	rsp := &dto.AccountResponse{}
	err = json.Unmarshal(b, rsp)
	if err != nil {
		return nil, err
	}

	return rsp, nil
}

// GetWorkflowStatus
// https://retrieval.apac-1.jumio.ai/api/v1/accounts/<accountId>/workflow-executions/<workflowExecutionId>/status
func (s *JumioProvider) GetWorkflowStatus(ctx context.Context, accountID, workflowExecutionID string) (*dto.WorkflowStatusResponse, error) {
	var err error
	path := s.conf.WorkflowDomain + "/api/v1/accounts/" + accountID + "/workflow-executions/" + workflowExecutionID + "/status"
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetWorkflowStatus[jumio]:"+path, err)
		}
	}()
	b, err := s.request(ctx, commonHeader, path, "GET", nil)
	if err != nil {
		return nil, err
	}
	rsp := &dto.WorkflowStatusResponse{}
	err = json.Unmarshal(b, rsp)
	if err != nil {
		return nil, err
	}

	return rsp, nil
}

// GetWorkflowDetails
// https://retrieval.apac-1.jumio.ai/api/v1/accounts/<accountId>/workflow-executions/<workflowExecutionId>
func (s *JumioProvider) GetWorkflowDetails(ctx context.Context, accountID, workflowExecutionID string) (*dto.WorkflowDetailsResponse, error) {
	var err error
	path := s.conf.WorkflowDomain + "/api/v1/accounts/" + accountID + "/workflow-executions/" + workflowExecutionID
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetWorkflowDetails[jumio]:"+path, err)
		}
	}()
	b, err := s.request(ctx, commonHeader, path, "GET", nil)
	if err != nil {
		return nil, err
	}

	s.logger.Debug(string(b))

	rsp := &dto.WorkflowDetailsResponse{}
	err = json.Unmarshal(b, rsp)
	if err != nil {
		return nil, err
	}

	return rsp, nil
}

// GetWorkflowImage https://retrieval.apac-1.jumio.ai/api/v1/accounts/<accountId>/credentials/<credentialId>/parts/<classifier>
// classifier(分类器): FRONT  BACK  FACE  FACEMAP
func (s *JumioProvider) GetWorkflowImage(ctx context.Context, accountID, credentialID, classifier string) ([]byte, error) {
	var err error
	path := s.conf.WorkflowDomain + "/api/v1/accounts/" + accountID + "/credentials/" + credentialID + "/parts/" + classifier
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetWorkflowImage[jumio]:"+path, err)
		}
	}()
	b, err := s.DownloadImage(ctx, path)
	if err != nil {
		return nil, err
	}

	return b, nil
}

// DocumentAcquisitions https://acquisition.core-sgp.jumio.com/api/netverify/v2/acquisitions
func (s *JumioProvider) DocumentAcquisitions(ctx context.Context, memberID, country, docType string, successURL, errorURL, language string) (*dto.DocumentAcquisitionsResponse, error) {
	var err error
	path := s.conf.DocumentVerificationDomain + "/api/netverify/v2/acquisitions"
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "DocumentAcquisitions[jumio]:"+path, err)
		}
	}()
	req := &dto.DocumentAcquisitionsRequest{
		Type:                  docType,
		Country:               country,
		MerchantScanReference: memberID,
		CustomerID:            memberID,
		SuccessURL:            successURL,
		ErrorURL:              errorURL,
		CallbackURL:           s.conf.CallBackDocument,
	}
	if v, ok := LanguageBybitToJumioMap[language]; ok {
		req.Locale = v
	} else {
		req.Locale = defaultLanguageEn
	}
	body, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	s.logger.Debug("DocumentAcquisitions[jumio]", zap.String("language:", language))
	s.logger.Debug("DocumentAcquisitions[jumio]", zap.String("req", string(body)))
	b, err := s.request(ctx, commonHeader, path, "POST", body)
	if err != nil {
		return nil, err
	}
	rsp := new(dto.DocumentAcquisitionsResponse)
	err = json.Unmarshal(b, rsp)
	if err != nil {
		return nil, err
	}
	if rsp.ScanReference == "" {
		if rsp.DocumentVerificationResponseHeader.Message != "" {
			return nil, errors.New(rsp.DocumentVerificationResponseHeader.Message)
		}
		return nil, errors.New("ScanReference is null ")
	}
	// 当rsp.Message != "" 表示接口有问题, 调用接口的地方需要判断错误
	return rsp, nil
}

// GetDocumentDetail https://retrieval.core-sgp.jumio.com/api/netverify/v2/documents/<scanReference>/data
func (s *JumioProvider) GetDocumentDetail(ctx context.Context, scanReference string) (*dto.GetDocumentDetailResponse, error) {
	var err error
	path := s.conf.RetrievalDomain + "/api/netverify/v2/documents/" + scanReference + "/data"
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetDocumentDetail[jumio]:"+path, err)
		}
	}()
	b, err := s.request(ctx, commonHeader, path, "GET", nil)
	if err != nil {
		return nil, err
	}

	rsp := new(dto.GetDocumentDetailResponse)
	err = json.Unmarshal(b, rsp)
	if err != nil {
		return nil, err
	}
	// 调用方法的地方需要判断rsp.HttpStatus; 404表示不存在此数据
	return rsp, nil
}

// GetDocumentStatus https://retrieval.core-sgp.jumio.com/api/netverify/v2/documents/<scanReference>
func (s *JumioProvider) GetDocumentStatus(ctx context.Context, scanReference string) (*dto.GetDocumentStatusResponse, error) {
	var err error
	path := s.conf.RetrievalDomain + "/api/netverify/v2/documents/" + scanReference
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetDocumentStatus[jumio]:"+path, err)
		}
	}()
	b, err := s.request(ctx, commonHeader, path, "GET", nil)
	if err != nil {
		return nil, err
	}

	rsp := new(dto.GetDocumentStatusResponse)
	err = json.Unmarshal(b, rsp)
	if err != nil {
		return nil, err
	}
	// 调用方法的地方需要判断rsp.HttpStatus; 400标识bad request
	return rsp, nil
}

// GetDocumentImages https://retrieval.core-sgp.jumio.com/api/netverify/v2/documents/<scanReference>/pages
func (s *JumioProvider) GetDocumentImages(ctx context.Context, scanReference string) (*dto.GetDocumentImagesResponse, error) {
	var err error
	path := s.conf.RetrievalDomain + "/api/netverify/v2/documents/" + scanReference + "/pages"
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetDocumentImages[jumio]:"+path, err)
		}
	}()
	b, err := s.request(ctx, commonHeader, path, "GET", nil)
	if err != nil {
		return nil, err
	}

	rsp := new(dto.GetDocumentImagesResponse)
	err = json.Unmarshal(b, rsp)
	if err != nil {
		return nil, err
	}
	// 调用方法的地方需要判断rsp.HttpStatus; 404表示不存在此数据
	return rsp, nil
}

// GetDocumentSpecificImage https://retrieval.core-sgp.jumio.com/api/netverify/v2/documents/<scanReference>/pages/<page_number>
func (s *JumioProvider) GetDocumentSpecificImage(ctx context.Context, scanReference string, page int) ([]byte, error) {
	var err error
	path := s.conf.RetrievalDomain + "/api/netverify/v2/documents/" + scanReference + "/pages/" + strconv.Itoa(page)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetDocumentImages[jumio]:"+path, err)
		}
	}()

	b, err := s.DownloadImage(ctx, path)
	if err != nil {
		return nil, err
	}

	return b, nil
}

func (s *JumioProvider) SupportedDocumentTypes(ctx context.Context) (*dto.SupportedDocumentTypes, error) {
	var err error
	path := s.conf.CoreDomain
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "SupportedDocumentTypes[jumio]:"+path, err)
		}
	}()
	b, err := s.request(ctx, commonHeader, path, "GET", nil)
	if err != nil {
		return nil, err
	}

	rsp := new(dto.SupportedDocumentTypes)
	err = json.Unmarshal(b, rsp)
	if err != nil {
		return nil, err
	}
	return rsp, nil
}

func (s *JumioProvider) DownloadImage(ctx context.Context, imageURL string) ([]byte, error) {
	return s.request(ctx, imageFetchHeader, imageURL, "GET", nil)
}

const ContentType = "application/json"

var commonHeader = map[string]string{
	"Content-Type": ContentType,
	"Connection":   "keep-alive",
}

var imageFetchHeader = map[string]string{
	"Accept":          "*/*",
	"Accept-Encoding": "gzip, deflate, br",
	"Connection":      "keep-alive",
}

func (s *JumioProvider) request(
	ctx context.Context,
	headerMap map[string]string,
	url, method string,
	body []byte,
) ([]byte, error) {
	logc.Infow(ctx, "request begin")
	// byone框架
	httpClient := &http.Client{
		Timeout: 10 * time.Second,
	}
	httpService := httpc.NewServiceWithClient("httpService", httpClient, func(r *http.Request) *http.Request {
		r.SetBasicAuth(s.conf.Token, s.conf.Secret)
		for k, v := range headerMap {
			r.Header.Set(k, v)
		}
		return r
	})
	// 重新构造请求
	httpReq, err := http.NewRequest(method, url, bytes.NewBuffer(body))
	if err != nil {
		return nil, err
	}
	response, err := httpService.DoRequest(httpReq)
	if err != nil {
		_ = teller.GlobTeller(ctx, "httpReq:"+url+" error:", err)
		logc.Errorw(ctx, "httpService.Do error", logc.Field("err", err))
		return nil, err
	}
	defer func() {
		if err = response.Body.Close(); err != nil {
			logc.Errorw(ctx, "HttpC response.Body.Close error", logc.Field("err", err))
		}
	}()
	logc.Infow(ctx, "HttpC client.Do success", logc.Field("status", response.Status))
	respBody, err := ioutil.ReadAll(response.Body)
	if err != nil {
		logc.Errorw(ctx, "HttpC ioutil.ReadAll error", logc.Field("err", err))
		return nil, err
	}
	if response.StatusCode != http.StatusOK {
		logc.Errorw(ctx, "client.Do respCode not success", logc.Field("respBody", string(respBody)))
		return respBody, nil
	}
	logc.Infow(ctx, "resuest end", logc.Field("respBody", string(respBody)))
	return respBody, nil
}

// nolint
func (s *JumioProvider) GetToken(ctx context.Context, hashID string, TTL int, level dto.KYCLevel) (string, error) {

	return "", nil
}
func (s *JumioProvider) GetApplicant(ctx context.Context, applicantID string) (interface{}, error) {
	return "", nil
}

func (s *JumioProvider) ShareTokens(ctx context.Context, applicantID, forClientID string) (string, error) {
	return "", nil
}

func convJumioDocType(docType dto.JUMIODocumentType) dto.DocumentType {
	appType := dto.DocumentTypeUnknown
	switch docType {
	case dto.JUMIODocumentTypePassport:
		appType = dto.DocumentTypePassport
	case dto.JUMIODocumentTypeDriver:
		appType = dto.DocumentTypeDriver
	case dto.JUMIODocumentTypeID:
		appType = dto.DocumentTypeID
	case dto.JUMIODocumentTypeVISA:
		appType = dto.DocumentTypeBank
	}

	return appType
}

func (s *JumioProvider) GetKYCAdminDetail(ctx context.Context, idempotentID string, ntf *kae_dto.KYCEventNotify, firstPoi *dto.Identify) (*dto.KYCAdminDetail, []*dto.KYCActionImage, error) {
	var (
		frontImgBucketStr, backImgBucketStr, faceImgBucketStr, livenessImgBucketStr string
	)
	if dto.KYCLevel(ntf.Level) == dto.Level2 {
		detail, err := s.GetDocumentDetail(ctx, ntf.RecordID)
		if err != nil {
			bzap.LogError(s.logger.With(
				zap.String("idempotentID", idempotentID),
				zap.Any("ntf", ntf),
			), "GetDocumentDetail", err)
			return nil, nil, err
		}

		docImgs, imgErr := s.GetKYCDocImages(ctx, idempotentID, ntf.RecordID)
		if imgErr != nil {
			s.logger.Error("GetKYCImages error",
				zap.Any("idempotentID", idempotentID),
				zap.Any("RecordID", ntf.RecordID),
				zap.Error(imgErr))
		}
		for id, doc := range docImgs {
			s.logger.Debug("docimg", zap.Any("id", id), zap.Any("doc", doc))
			doc.Data = []byte("")
			if doc.Tag == dto.ImageTagFront {
				frontImgBucketStr, _ = jsoniter.MarshalToString(doc)
			}
		}

		nested := detail.GetNestedExtractedData()

		// 格式化完整居住地址
		countryCode := nested.CountryCode
		if countryCode == "" {
			countryCode = detail.GetCountry()
		}
		fullCountry := fmt.Sprintf("%s ", countryCode)
		countryName := helper.CountryNameAlpha3(countryCode)
		if countryName.EnName != "" {
			fullCountry = fmt.Sprintf("%s ", countryName.EnName)
		}

		fullCity := nested.City
		if fullCity != "" {
			fullCity = fmt.Sprintf("%s ", fullCity)
		}
		fullSubdivision := nested.Subdivision
		if fullSubdivision != "" {
			fullSubdivision = fmt.Sprintf("%s ", fullSubdivision)
		}
		fullLine1 := nested.Line1
		if fullLine1 != "" {
			fullLine1 = fmt.Sprintf("%s ", fullLine1)
		}
		fullLine2 := nested.Line2
		if fullLine2 != "" {
			fullLine2 = fmt.Sprintf("%s ", fullLine2)
		}
		fullLine3 := nested.Line3
		if fullLine3 != "" {
			fullLine3 = fmt.Sprintf("%s ", fullLine3)
		}
		fullLine4 := nested.Line4
		if fullLine4 != "" {
			fullLine4 = fmt.Sprintf("%s ", fullLine4)
		}
		fullLine5 := nested.Line5

		fullAddress := fmt.Sprintf(
			"%s%s%s%s%s%s%s%s",
			fullCountry,
			fullCity,
			fullSubdivision,
			fullLine1,
			fullLine2,
			fullLine3,
			fullLine4,
			fullLine5,
		)
		fullAddress = strings.Trim(fullAddress, " ")
		kycDtl := &dto.KYCAdminDetail{
			ProviderDecision:     int8(detail.GetReviewResult()),
			ProviderRejectReason: detail.GetReviewLabelStr(),
			DocType:              int8(provider.GetUnifiedDocType(detail.Document.Type, dto.ProviderJumio)),
			DocSubtype:           0,
			DocNumber:            "",
			Country:              detail.GetCountry(),
			Nationality:          "",
			AddressCountry:       nested.CountryCode,
			FirstName:            detail.Document.ExtractedData.FirstName,
			LastName:             detail.Document.ExtractedData.LastName,
			Dob:                  "",
			DocDetail: dto.KYCAdminDocDetail{
				DocFront:       frontImgBucketStr,
				DocBack:        "",
				PersonalNumber: "",
			},
			IssuingDate: nested.IssueDate,
			ExpireDate:  nested.ExpiryDate,
			FaceDetail: dto.KYCAdminFaceDetail{
				FaceImage:         "",
				LivenessImage:     "",
				SimilarityChecked: "",
				ValidityChecked:   "",
			},
			AddressDetail: dto.KYCAdminAddressDetail{
				Country:     countryCode,
				State:       nested.Subdivision,
				City:        nested.City,
				FullAddress: fullAddress,
				PostalCode:  nested.PostalCode,
			},
			ClientDetail: dto.KYCAdminClientDetail{
				ClientIP: detail.Transaction.ClientIP,
				From:     "",
				Source:   detail.Transaction.Source,
			},
		}
		return kycDtl, nil, nil
	} else {
		arkDtl, err := s.GetWorkflowDetails(ctx, ntf.AccountID, ntf.RecordID)
		if err != nil {
			bzap.LogError(s.logger.With(
				zap.String("idempotentID", idempotentID),
				zap.Any("ntf", ntf),
			), "GetWorkflowDetails", err)
			return nil, nil, err
		}

		application := arkDtl.GetApplicantData()
		subType := dto.SubTypeDesc(arkDtl.GetSubType())
		adr := arkDtl.GetAddress()
		// 从 KYC 服务器下载；加密内容；上传到 AWS s3
		docImgs, imgErr := s.GetKYCImages(ctx, idempotentID, ntf.AccountID, arkDtl)
		if imgErr != nil {
			s.logger.Error("GetKYCImages error",
				zap.Any("idempotentID", idempotentID),
				zap.Any("AccountID", ntf.AccountID),
				zap.Error(imgErr))
		}

		for id, doc := range docImgs {
			s.logger.Debug("docimg", zap.Any("id", id), zap.Any("doc", doc))
			doc.Data = []byte("")
			switch doc.Tag {
			case dto.ImageTagFace:
				faceImgBucketStr, _ = jsoniter.MarshalToString(doc)
			case dto.ImageTagBack:
				backImgBucketStr, _ = jsoniter.MarshalToString(doc)
			case dto.ImageTagFront:
				frontImgBucketStr, _ = jsoniter.MarshalToString(doc)
			case dto.ImageTagLiveness:
				livenessImgBucketStr, _ = jsoniter.MarshalToString(doc)
			}
		}

		s.logger.Debug("DocType", zap.Any("application.Identities[0]", application.Identities))

		kycDtl := &dto.KYCAdminDetail{
			ProviderDecision:     int8(arkDtl.GetReviewResult()),
			ProviderRejectReason: arkDtl.GetReviewLabelStr(),
			DocType:              int8(provider.GetUnifiedDocType(arkDtl.GetDocType(), dto.ProviderJumio)),
			DocSubtype:           int8(subType.GetSubType()),
			DocNumber:            application.ID,
			Country:              application.Country,
			Nationality:          arkDtl.GetNationality(),
			AddressCountry:       adr.Country,
			FirstName:            application.Firstname,
			LastName:             application.Lastname,
			Dob:                  application.Dob,
			DocDetail: dto.KYCAdminDocDetail{
				DocFront:       frontImgBucketStr,
				DocBack:        backImgBucketStr,
				PersonalNumber: arkDtl.GetPersonalNumber(),
			},
			IssuingDate: arkDtl.GetIssuingDate(),
			ExpireDate:  application.Identities[0].Validuntil,
			FaceDetail: dto.KYCAdminFaceDetail{
				FaceImage:         faceImgBucketStr,
				LivenessImage:     livenessImgBucketStr,
				SimilarityChecked: arkDtl.GetSimilarity(),
				ValidityChecked:   arkDtl.IsValidityChecked(),
			},
			AddressDetail: dto.KYCAdminAddressDetail{
				Country:     adr.Country,
				State:       arkDtl.GetAddressState(),
				City:        adr.City,
				FullAddress: adr.FormattedAddress,
				PostalCode:  adr.PostalCode,
			},
		}
		if application.PassTime != "" && ntf.From == kae_dto.EventSourceMigrate { // 只处理来自迁移的消息
			ft, errTm := time.Parse("2006-01-02T15:04:05.999Z", application.PassTime)
			if errTm == nil {
				kycDtl.ReviewAtE3 = ft.UnixNano() / int64(time.Millisecond)
			}
		}

		return kycDtl, nil, nil
	}
}

func (s *JumioProvider) GetDocuments(ctx context.Context, accountID string, wrkDtl *dto.WorkflowDetailsResponse, ch chan<- *dto.Document) error {
	var err error
	defer close(ch)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetDocuments[jumio]", err)
		}
	}()

	for _, cdl := range wrkDtl.Credentials {
		if cdl.Parts == nil {
			continue
		}
		credentialID := cdl.ID
		for _, prt := range *cdl.Parts {
			switch prt.Classifier {
			case dto.ImageTagBack, dto.ImageTagFront, dto.ImageTagLiveness, dto.ImageTagFace:
				f := helper.TimeCost(s.logger, "JumioProvider.GetWorkflowImage")
				b, err := s.GetWorkflowImage(ctx, accountID, credentialID, prt.Classifier)
				f()
				if err != nil {
					return err
				}
				doc := new(dto.Document)
				doc.ID = fmt.Sprintf("%s-%s", credentialID, strings.ToLower(prt.Classifier))
				doc.Data = b
				// 图片压缩
				nb, e := s.imgCps.CompressImg(ctx, b)
				if e != nil {
					bzap.LogError(s.logger.With(
						zap.String("accountID", accountID),
						zap.Any("Classifier", prt.Classifier),
					), "CompressImg-Jumio", e)
				} else {
					s.logger.Info("CompressImg-Jumio", zap.String("ImgID", doc.ID))
					doc.Data = nb
				}
				doc.Mime = http.DetectContentType(b)
				doc.Tag = prt.Classifier
				if s.logger != nil {
					s.logger.Info(fmt.Sprintf("classifier:%s, filsize:%d", prt.Classifier, len(doc.Data)))
				}
				doc.Type = convJumioDocType(dto.JUMIODocumentTypeID)
				s.logger.Debug("CreateDoc", zap.Any("doc", doc))
				select {
				case <-ctx.Done():
					return ctx.Err()
				case ch <- doc:
				}
			}
		}
	}

	return nil
}

func (s *JumioProvider) GetDocumentsImages(ctx context.Context, accountID string, docImgs *dto.GetDocumentImagesResponse, ch chan<- *dto.Document) error {
	var err error
	defer close(ch)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetDocumentsImages[jumio]", err)
		}
	}()

	for _, img := range docImgs.Images {
		f := helper.TimeCost(s.logger, "JumioProvider.GetDocumentSpecificImage")
		b, err := s.GetDocumentSpecificImage(ctx, accountID, img.Classifier)
		f()
		if err != nil {
			return err
		}
		doc := new(dto.Document)
		doc.ID = fmt.Sprintf("%s-%s", accountID, strings.ToLower(strconv.Itoa(img.Classifier)))
		doc.Data = b
		// 图片压缩
		nb, e := s.imgCps.CompressImg(ctx, b)
		if e != nil {
			bzap.LogError(s.logger.With(
				zap.String("accountID", accountID),
				zap.Any("Classifier", img.Classifier),
			), "CompressImg-Jumio", e)
		} else {
			s.logger.Info("CompressImg-Jumio", zap.String("ImgID", doc.ID))
			doc.Data = nb
		}
		doc.Mime = http.DetectContentType(b)
		doc.Tag = dto.ImageTagFront
		if s.logger != nil {
			s.logger.Info(fmt.Sprintf("classifier:%d, filsize:%d", img.Classifier, len(doc.Data)))
		}
		doc.Type = convJumioDocType(dto.JUMIODocumentTypeID)
		s.logger.Debug("CreateDoc", zap.Any("doc", doc))
		select {
		case <-ctx.Done():
			return ctx.Err()
		case ch <- doc:
		}
	}

	return nil
}

func (s *JumioProvider) GetKYCDocImages(ctx context.Context, idempotentID, recordID string) (map[string]*dto.Document, error) {
	docImgs, err := s.GetDocumentImages(ctx, recordID)
	if err != nil {
		return nil, err
	}
	if len(docImgs.Images) < 1 {
		return nil, nil
	}
	ch := make(chan *dto.Document, 2) // front back face liveness

	go func() {
		defer helper.TimeCost(s.logger, "provider.GetDocuments")()
		e := s.GetDocumentsImages(ctx, recordID, docImgs, ch)
		if e != nil {
			bzap.LogError(s.logger, "provider.GetDocuments error", e)
		}
	}()

	if docs, e := uploadDocument(ctx, s.logger, idempotentID, ch); e != nil {
		bzap.LogError(s.logger, "uploadDocument error", e)
		return nil, e
	} else {
		s.logger.Debug("documents", zap.Any("docs", docs))
		return docs, nil
	}
}

func (s *JumioProvider) GetImageRaw(ctx context.Context, actionImg *dto.KYCActionImage) (*dto.Document, error) {
	return nil, nil
}

func (s *JumioProvider) GetKYCImages(ctx context.Context, idempotentID, accountID string, wrkDtl *dto.WorkflowDetailsResponse) (map[string]*dto.Document, error) {
	if len(wrkDtl.Credentials) < 1 {
		return nil, errors.New("No credentials")
	}
	ch := make(chan *dto.Document, 4) // front back face liveness

	go func(accountID string, wrkDtl *dto.WorkflowDetailsResponse) {
		defer helper.TimeCost(s.logger, "provider.GetDocuments")()
		e := s.GetDocuments(ctx, accountID, wrkDtl, ch)
		if e != nil {
			bzap.LogError(s.logger, "provider.GetDocuments error", e)
		}
	}(accountID, wrkDtl)

	if docs, e := uploadDocument(ctx, s.logger, idempotentID, ch); e != nil {
		bzap.LogError(s.logger, "uploadDocument error", e)
		return nil, e
	} else {
		s.logger.Debug("documents", zap.Any("docs", docs))
		return docs, nil
	}
}

func uploadDocument(ctx context.Context, logger *zap.Logger, hashID string, ch <-chan *dto.Document) (map[string]*dto.Document, error) {
	var (
		secret = bsecretsecret.Secret([]byte(hashID))
		bucket = byoneconfig.GetAWSS3Config().Bucket
	)

	sess, err := helper.GetAWSS3Session()
	if err != nil {
		return nil, err
	}

	uploader := s3manager.NewUploader(sess)
	docs := make(map[string]*dto.Document)

	for doc := range ch {
		logger.Debug("upload start", zap.String("doc.Id", doc.ID), zap.String("doc.tag", doc.Tag))
		doc.Key = fmt.Sprintf("%s/%s", hashID, doc.ID) // hash(uid) && doc.id
		doc.Bucket = bucket
		docs[doc.ID] = doc
		body, e := secret.Seal(doc.Data)
		if e != nil {
			return nil, err
		}
		f := helper.TimeCost(logger, "UploadWithContext")
		_, err := uploader.UploadWithContext(ctx, &s3manager.UploadInput{
			Bucket: aws.String(bucket),
			Key:    aws.String(doc.Key),
			Body:   bytes.NewReader(body),
		})
		f()
		if err != nil {
			logger.Error("upload error", zap.String("doc.Id", doc.ID), zap.String("doc.tag", doc.Tag))
			return nil, err
		}
	}

	return docs, nil
}

func (s *JumioProvider) GetKYCOnboardingPOIActionImages(ctx context.Context, memberID int64, accountID, recordID, idempotentID string) (actionImgs []*dto.KYCActionImage, err error) {
	return nil, nil
}
